////////////////////////////////////////////////////////////////////////////////
//
//  The MIT License
//
//  Copyright (c) 2008 - 2009. Dimarik
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
//
////////////////////////////////////////////////////////////////////////////////

package tx.framework.service {

	import flash.errors.IllegalOperationError;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.events.TimerEvent;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.net.URLRequestMethod;
	import flash.net.URLVariables;
	import flash.utils.Timer;

	import tx.framework.logging.Log;
	import tx.framework.logging.LogEventLevel;
	import tx.framework.utils.MD5;

	/**
	 *  <p>Класс позволяет работать с API популярной социальной сети <b>"Мой Мир"</b> http://my.mail.ru/</p>
	 *  <p>Для получения детальной информации о работе с API ознакомьтесь с документацией
	 *  по адресу http://my.mail.ru/simple_api</p>
	 *
	 *  @author				Dimarik
	 *  @version			1.0
	 *  @langversion		3.0
	 *  @playerversion		9.0
	 *
	 */
	public class MMService implements IService {

		//---------------------------------------------------------------------
		//
		//   Public class's constants
		//
		//---------------------------------------------------------------------

		//-----------------------------
		// API methods
		//-----------------------------

		public static const METHOD_GET_VARIABLE:		String = 'getVariable';

		public static const METHOD_PUT_VARIABLE:		String = 'putVariable';

		public static const METHOD_GET_USER_INFO:		String = 'getUserInfo';

		public static const METHOD_GET_APP_FRIENDS:		String = 'getAppFriends';

		public static const METHOD_GET_FRIENDS:			String = 'getFriends';

		public static const METHOD_GET_PROFILES:		String = 'getProfiles';

		public static const METHOD_GET_SERVER_TIME:		String = 'getServerTime';

		//-----------------------------
		// API methods argument's names
		//-----------------------------

		public static const FIELD_PHOTO:				String = 'photo';

		public static const FIELD_PHOTO_MEDIUM:			String = 'photo_medium';

		public static const FIELD_PHOTO_BIG:			String = 'photo_big';

		public static const FIELD_UID:					String = 'uid';

		public static const FIELD_FIRST_NAME:			String = 'first_name';

		public static const FIELD_LAST_NAME:			String = 'last_name';

		public static const FIELD_NICKNAME:				String = 'nickname';

		public static const FIELD_SEX:					String = 'sex';

		public static const FIELD_BDATE:				String = 'bdate';

		public static const FIELD_CITY:					String = 'city';

		public static const FIELD_TIMEZONE:				String = 'timezone';


		public static const CELL_PASSWORD:				uint = 1300;


		//---------------------------------------------------------------------
		//
		//   Public class's variables
		//
		//---------------------------------------------------------------------

		public static var API_URL:	String 	= 'http://appsmail.ru/myapi/';

		public static var MIN_REQUESTS_DELAY:	Number = (1/3)  *  1e3;

		//---------------------------------------------------------------------
		//
		//   Private class's constants
		//
		//---------------------------------------------------------------------

		/**
		 * @private
		 */
		private static const _timer:	Timer = new Timer(MIN_REQUESTS_DELAY, 1);

		//---------------------------------------------------------------------
		//
		//   Private class's variables
		//
		//---------------------------------------------------------------------

		/**
		 * @private
		 */
		private static var _instance:		MMService;
		
		private static var _instantiated:	Boolean;

		//---------------------------------------------------------------------
		//
		//   Class's methods
		//
		//---------------------------------------------------------------------

		public static function getInstance():MMService {
			if (_instance == null) {
				_instantiated = true;
				_instance = new MMService();
			}
			return _instance;
		}

		//---------------------------------------------------------------------
		//
		//   Constructor
		//
		//---------------------------------------------------------------------

		/**
		 * Constructor
		 */
		public function MMService() {
			super();

			if (!_instantiated || _instance) {
				throw new IllegalOperationError('MMService is singleton. Use MMService.getInstance() instead');
			}

			this._loader = new URLLoader();
			this._loader.dataFormat = URLLoaderDataFormat.TEXT;
			this._loader.addEventListener(Event.COMPLETE, 						this.handler_success);
			this._loader.addEventListener(IOErrorEvent.IO_ERROR,				this.handler_fault);
			this._loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR,	this.handler_fault);

		}

		//---------------------------------------------------------------------
		//
		//   Properties
		//
		//---------------------------------------------------------------------

		//-----------------------------
		//  currentMethod
		//-----------------------------
		/**
		 * @private
		 */
		private var _currentMethod:	String;

		public function get currentMethod():String {
			return this._currentMethod;
		}

		//---------------------------------------------------------------------
		//
		//   Public variables
		//
		//---------------------------------------------------------------------
/*
		public var is_app_user:	Boolean;

		public var api_id:		uint;

		public var viewer_id:	uint;

		public var auth_key:	String;

		public var test_mode:	Boolean;

		public var app_secret:	String;

		public var user_id:		uint;

		public var viewer_type:	uint;

		public var group_id:	uint;

		public var language:	uint;

		public var userName:	String;
*/

		public var is_app_user:	Boolean;

		public var token:		String;

		public var api_id:		uint;

		public var viewer_id:	uint;

		public var auth_key:	String;

		public var app_secret:	String;

		public var test_mode:	Boolean;

		public var userName:	String;

		//---------------------------------------------------------------------
		//
		//   Private constants
		//
		//---------------------------------------------------------------------

		/**
		 * @private
		 */
		private const _queue:			Array = new Array();

		//---------------------------------------------------------------------
		//
		//   Private variables
		//
		//---------------------------------------------------------------------

		/**
		 * @private
		 */
		private var _currentRequest:	Object;

		/**
		 * @private
		 */
		private var _loader:			URLLoader;

		//---------------------------------------------------------------------
		//
		//   Interface implementation: IService
		//
		//---------------------------------------------------------------------

		public function configurate(params:Array):void {
			// do nothing
		}

		//------------------------------------------
		// Common Remote Procedure Call
		//------------------------------------------
		public function call(apiMethod:String, responder:Object, options:Object = null):void {
			var requestObject:Object = new Object();
			requestObject.responder = responder;
			requestObject.method = apiMethod;
			requestObject.options = options;

			this._currentMethod = apiMethod;

			this._queue.push(requestObject);

			if( !this._currentRequest ) this.requestNext();
		}

		//------------------------------------------
		//  Load data
		//------------------------------------------
		public function load(responder:Object, options:Object = null):void {
			var requestObject:Object = new Object();
			requestObject.responder = responder;
			requestObject.method = '';
			requestObject.options = options;

			this._queue.push(requestObject);

			if( !this._currentRequest ) this.requestNext();
		}

		/**
		 * 
		 * @param value	method name
		 * 
		 * @return 		reference to particular service method
		 * 
		 */
		public function getParticularMethod(value:String):Function {
			if (this[value] is Function) {
				return this[value] as Function;
			}
			return null;
		}

		/**
		 * 
		 * @param value	method name
		 * 
		 * @return 		has method
		 * 
		 */
		public function hasParticularMethod(value:String):Boolean {
			return this[value] is Function;
		}

		//------------------------------------------
		// Particular Remote Procedure Call methods
		//------------------------------------------

		public function getUserInfo(responder:Object, uid:String):void {
			var options:Object = 
					{
						uids	: uid
					};
			this.call(METHOD_GET_PROFILES, responder, options);
		}

		public function setPassword(responder:Object, password:String):void {
			this.setVariable(responder, CELL_PASSWORD, password);
		}

		public function getPassword(responder:Object):void {
			this.getVariable(responder, CELL_PASSWORD);
		}

		public function getVariable(responder:Object, variable:int):void {
			this.call(METHOD_GET_VARIABLE, responder, {key:variable});
		}

		public function setVariable(responder:Object, variable:int, value:String):void {
			this.call(METHOD_PUT_VARIABLE, responder, {key:variable, value:value});
		}

		public function getAppFriends(responder:Object):void {
			this.call(METHOD_GET_APP_FRIENDS, responder);
		}

		public function getServerTime(responder:Object):void {
			this.call(METHOD_GET_SERVER_TIME, responder);
		}

		public function getFriends(responder:Object):void {
			this.call(METHOD_GET_FRIENDS, responder);
		}

		public function getProfiles(responder:Object, uids:String):void {
			var options:Object = 
					{
						uids	: uids
					};

			this.call(METHOD_GET_PROFILES, responder, options);
		}

		//---------------------------------------------------------------------
		//
		//   Private methods
		//
		//---------------------------------------------------------------------

		/**
		 * @private
		 */
		private function requestNext():void {
			if (this._queue.length == 0) return;

			if (_timer.running) {
				if (!_timer.hasEventListener(TimerEvent.TIMER_COMPLETE)) {
					_timer.addEventListener(TimerEvent.TIMER_COMPLETE, this.handler_timerComplete);
				}
				return;
			} else {
				_timer.reset();
				_timer.start();
			}

			this._currentRequest = this._queue.shift();

			var method:		String = this._currentRequest.method;
			var options:	Object = this._currentRequest.options;

			var params:		Object = {
			  	method	: method,
			  	api_id	: this.api_id,
			  	token	: this.token
			};

			if (this.test_mode) params['test_mode'] = '1';

			var i:String;

		    if (options) {
		    	for (i in options) params[i] = options[i];
			}

	      	var variables:URLVariables = new URLVariables();
			for (i in params) variables[i] = params[i];

			variables['sig'] = this.generateSignature(params);

			var request:URLRequest = new URLRequest();
			request.url 		   = API_URL;
			request.method 		   = URLRequestMethod.POST;
			request.data 		   = variables;

			var arr:Array = new Array(LogEventLevel.INFO, method + ' ' + variables.toString());
			Log.getLogger("MM-OUT").log.apply(null, arr);

			this._loader.load(request);
	    }

		/**
		 * Generate signature
		 * 
		 * @private
		 * @return	MD5 crypted string
		 */
		private function generateSignature(requestParams:Object):String {
			var stringToEncode:		String = '';
			var sortedArray:		Array = new Array();

			for (var key:String in requestParams) {
				sortedArray.push(key + '=' + requestParams[key]);
			}

			sortedArray.sort();

			for (key in sortedArray) {
				stringToEncode += sortedArray[key];
			}

			stringToEncode = String(this.viewer_id) + stringToEncode + this.app_secret;

			return MD5.encrypt(stringToEncode);
		}

		//---------------------------------------------------------------------
		//
		//   Event handlers
		//
		//---------------------------------------------------------------------

		/**
		 * @private
		 */
		protected function handler_success(event:Event):void {
			this._currentRequest.responder.result( (event.target as URLLoader).data );
			this._currentRequest = null;
			this.requestNext();
		}

		/**
		 * @private
		 */
		protected function handler_fault(event:Event):void {
			var loader:URLLoader = event.currentTarget as URLLoader;
			this._currentRequest.responder.fault(loader.data);
			this._currentRequest = null;
			this.requestNext();
		}

		/**
		 * @private
		 */
		private function handler_timerComplete(event:Event):void {
			_timer.removeEventListener(TimerEvent.TIMER_COMPLETE, this.handler_timerComplete);
			this.requestNext();
		}

	}
}
